package edu.kit.aifb.ldstream.learner.tasks;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.logging.Logger;

import org.openrdf.model.BNode;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.model.vocabulary.RDFS;
import org.openrdf.query.BindingSet;
import org.openrdf.query.impl.MutableTupleQueryResult;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.RepositoryResult;
import org.openrdf.rio.ParserConfig;
import org.openrdf.rio.RDFParser;

import edu.kit.aifb.ldstream.env.Environment;
import edu.kit.aifb.ldstream.learner.dao.Instance;
import edu.kit.aifb.ldstream.learner.dao.SimpleStatistics;
import edu.kit.aifb.ldstream.learner.dao.Type;
import edu.kit.aifb.ldstream.learner.query.SPARQLQuery;
import edu.kit.aifb.ldstream.stat.util.IDistributionChecker;

public class LearnInstancesTask extends LearnerTask {

	private ExecutorService threadPool;
	private Map<Type, ArrayList<Instance>> classMap;
	private SimpleStatistics stats;
	private IDistributionChecker distribCheck;
	private boolean addingStats;
	private boolean inferencing;

	public LearnInstancesTask(ExecutorService threadPool, Map<Type, ArrayList<Instance>> classMap,
			SimpleStatistics stats, IDistributionChecker distribCheck,
			boolean inferencing, boolean addingStats){
		this.threadPool = threadPool;
		this.classMap = classMap;
		this.stats = stats;
		this.distribCheck = distribCheck;
		this.inferencing = inferencing;
		this.addingStats = addingStats;
	}

	@Override
	public void run() {
		startup();
		if(Environment.VERBOSE){
			System.out.println("Lerning the instances and types.");
		}
		learnInstances();
		countOccurrences();
		if(this.inferencing){
			addInferences();
		}
		if(this.addingStats){
			addingSimpleStatistics();
		}
		if(Environment.VERBOSE){
			System.out.println("Instances and types learned.");
		}
		shutdown();

	}

	private void addingSimpleStatistics(){
		// average number of instances
		int numInstances = 0;
		// list to calculate the average number of instances
		List<Integer> numInstancesList = new LinkedList<Integer>();
		for(Type t : this.classMap.keySet()){
			// getting the number of all instances of a class
			numInstances += this.classMap.get(t).size();
			numInstancesList.add(this.classMap.get(t).size());
		}
		this.stats.setAverageNumOfInstances(((double) numInstances) / this.classMap.size());
		// converting to an array for the distribution check
		Integer[] values = new Integer[numInstancesList.size()];
		values = numInstancesList.toArray(values);
		this.stats.setInstanceDistributions(this.distribCheck.discrete(values, false));
		this.stats.setNumTypes(this.classMap.size());
		System.out.println("NumTypes " + this.classMap.size());

	}

	private void countOccurrences(){
		RepositoryResult<Statement> result = null;
		Statement s = null;
		URI pred;
		try {
			for(Type t : this.classMap.keySet()){
				for(Instance i : this.classMap.get(t)){
					result = this.con.getStatements(i.getURI(), null, null, false, Environment.STREAM_CONTEXT_URI);
					try{
						while(result.hasNext()){
							s = result.next();
							pred = s.getPredicate();
							if(!pred.equals(RDF.TYPE)){
								i.incrementOccurence();
							}
						}
					}finally{
						result.close();
					}
				}
			}


		} catch (RepositoryException e) {
			e.printStackTrace();
		}

	}

	/*
	 * Used to get the instance => type definitions due to inferencing. 
	 * They could not be found in the first run because of the needed context. 
	 */
	private void addInferences(){
		Set<Instance> instanceSet = new HashSet<Instance>();
		// getting all instances first. No need to scan the same instance twice.
		for(Type t : this.classMap.keySet()){
			for(Instance i : this.classMap.get(t)){
				instanceSet.add(i);
			}
		}
		Statement s;
		URI pred;
		Type type;
		// scan every outgoing property for type definition
		for(Instance currentInstance : instanceSet){

			try {
				RepositoryResult<Statement> result = con.getStatements(currentInstance.getURI(), null, null, true);
				try{
					while(result.hasNext()){
						s = result.next();
						pred = s.getPredicate();
						// if the predicate matches with these strings we have a type definition
						if(pred.stringValue().contains("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
								|| pred.stringValue().contains("rdf:type")){
							// now check if it's a new one
							type = new Type((URI)s.getObject());
							if(!this.classMap.containsKey(type)){
								// we found a new type due to inferencing
								this.classMap.put(type, new ArrayList<Instance>());
							}
							// add the instance to the type if not already happend
							this.classMap.get(type).add(currentInstance);
						} /* else{
							// if the predicate is not a type definition it could still be linking to a bnode
							if(s.getObject() instanceof BNode){
								// crawl
								addInferences(currentInstance, (BNode) s.getObject());
							}
						}
						 */
					}
				}finally{
					result.close();
				}
			} catch (RepositoryException e) {
				e.printStackTrace();
			}
		}
	}

	private void addInferences(Instance currentInstance, BNode b){
		try {
			RepositoryResult<Statement> result = this.con.getStatements(b, null, null, true);
			Statement s;
			URI pred;
			Type type;
			try{
				while(result.hasNext()){
					s = result.next();
					pred = s.getPredicate();
					if(pred.stringValue().contains("http://www.w3.org/1999/02/22-rdf-syntax-ns#type") 
							|| pred.stringValue().contains("rdf:type")){
						// type definition !!
						type = new Type((URI)s.getObject());
						if(!this.classMap.containsKey(type)){
							// we found a new type due to inferencing
							this.classMap.put(type, new ArrayList<Instance>());
						}
						// adding the instance to type if not already happened
						this.classMap.get(type).add(currentInstance);
					} /*else{
						// no type definition but possible bnode connection
						if(s.getObject() instanceof BNode){
							// crawl this bnode
							addInferences(currentInstance, (BNode) s.getObject());
						}
					}
					 */
				}
			}finally{
				result.close();
			}
		} catch (RepositoryException e) {
			e.printStackTrace();
		}
	}


	private void learnInstances() {

		// RepositoryConnection repConnection = null;
		SPARQLQuery initQuery = null;
		SPARQLQuery instanceQuery = null;
		try {
			// sending query to the repository via QueryThread
			
			String initQueryS = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> " 
					+	"SELECT  $instance $class " 
					+	" FROM <" + Environment.STREAM_CONTEXT_URI.stringValue() + "> "
					+	"WHERE { $instance rdf:type $class FILTER( ! isBLANK($instance) && " 
					+	"!sameTerm($class, <" + RDFS.CLASS.stringValue() +"> )  " 
					+	" && !sameTerm($class, <" + RDF.PROPERTY 	+"> ) ) }";

			initQuery = new SPARQLQuery(Environment.REPO.getConnection(), initQueryS, this.inferencing);

			/*
			// assume that the URI which is bound to a bnode is an instance.
			String bnodeInstanceQuery = "SELECT $instance $bnode " 
										+ " FROM <" + Environment.STREAM_CONTEXT_URI.stringValue() + "> "
										+ "WHERE { $instance $pred $bnode FILTER(  isBLANK($bnode) && "
										+ "! isBLANK($instance) ) }";
			// needed to set preserve BNodes true
			this.con.setParserConfig(new ParserConfig(true, true, true, RDFParser.DatatypeHandling.VERIFY));
			instanceQuery = new SPARQLQuery(this.con, bnodeInstanceQuery, this.inferencing);
			// we must not close the connection until the instances are added. Otherwise the blank node labels will change.
			instanceQuery.disableAutoClose();
			 */
		} catch (RepositoryException e) {
			e.printStackTrace();
		}

		if(Environment.VERBOSE){
			System.out.println("LeanInstancesQuery send");
		}
		Future<MutableTupleQueryResult> futureClassResult = this.threadPool.submit(initQuery);
		//Future<MutableTupleQueryResult> futureInstanceResult = this.threadPool.submit(instanceQuery);
		learnInstances(futureClassResult);
		//addBnodeInstances(futureInstanceResult);
	}

	private void learnInstances(Future<MutableTupleQueryResult> futureClassResult){
		MutableTupleQueryResult classResult = null;
		try {
			classResult = futureClassResult.get();
			if(Environment.VERBOSE){
				System.out.println("LeanInstancesQuery received");
				System.out.println("NumInstances: " + classResult.size());
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}

		try {
			Type type;
			URI instance;
			List<Instance> tempList;

			int i = 0;
			int tenth = 0;
			double percentage = 0;

			if(Environment.LOGGING){
				tenth = Math.max(classResult.size() / 10, 1);
				percentage = 100d / classResult.size();
			}
			while (classResult.hasNext()) {

				if(Environment.LOGGING){
					if((i % tenth) == 0){
						Environment.log.info("Classes learned to: " + i * percentage + "%");
					}
					i++;
				}
				// Adding all classes and instances.
				BindingSet set = classResult.next();
				type = new Type((URI) set.getValue("class"));
				if (!this.classMap.containsKey(type)) {
					this.classMap.put(type, new ArrayList<Instance>());
				}
				// add found instance to the given class. Instances are saved as
				// a Set so no checks are needed.
				instance = (URI) set.getValue("instance");
				//adding the instance to the class
				Instance newInstance = new Instance(instance);
				if(!this.classMap.get(type).contains(newInstance)){
					this.classMap.get(type).add(new Instance(instance));
				}


			}

		} finally {
			classResult.close();
		}
	}

	private void addBnodeInstances(Future<MutableTupleQueryResult> futureInstanceResult){
		// adding all instances where rdf:type is behind a blank node.

		MutableTupleQueryResult instanceResult = null;
		try {
			instanceResult = futureInstanceResult.get();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}

		URI instance;
		BNode bnode;
		BindingSet set;
		try{
			while(instanceResult.hasNext()){
				set = instanceResult.next();
				instance = (URI) set.getValue("instance");
				bnode = (BNode) set.getValue("bnode");
				// this method will crawl over the bnodes looking for rdf:type and add the new instances.
				addBnodeInstances(instance, bnode);
			}
		}finally{
			instanceResult.close();
		}

	}

	private void addBnodeInstances(URI instance, BNode b){

		RepositoryResult<Statement> result = null; 
		try{
			result = this.con.getStatements(b, null, null, this.inferencing, Environment.STREAM_CONTEXT_URI);
			BNode b2;
			Value obj;
			Type objType;
			Statement s;
			String pred;
			try{
				while(result.hasNext()){
					s = result.next();
					pred = s.getPredicate().stringValue();
					obj = s.getObject();
					if(obj instanceof BNode){
						// we need to crawl further if we have a bnode her
						b2 = (BNode) obj;
						addBnodeInstances(instance, b2);
					}else if(pred.contains("http://www.w3.org/1999/02/22-rdf-syntax-ns#type") 
							|| pred.contains("rdf:type")	){

						// we found a new type definition!
						objType = new Type((URI)obj);
						if(!this.classMap.containsKey(objType)){
							//	System.err.println("found a new class in a later run. should not happen!");
							this.classMap.put(objType,new ArrayList<Instance>());
						}
						this.classMap.get(objType).add(new Instance(instance));

					}
				}
			}finally{
				result.close();
			}
		}catch(RepositoryException e){
			e.printStackTrace();
		}

	}


}
